package cloud.xuxiaowei.passport.controller;

import cloud.xuxiaowei.utils.Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * 自定义 错误处理
 *
 * @see AbstractErrorController
 * @see BasicErrorController
 * @see ErrorMvcAutoConfiguration#basicErrorController(ErrorAttributes, ObjectProvider)
 * @author xuxiaowei
 * @since 0.0.1
 */
@Slf4j
@RestController
@RequestMapping({ "${server.error.path:${error.path:/error}}" })
public class CustomErrorController extends AbstractErrorController {

	private final ErrorAttributes errorAttributes;

	private final ErrorProperties errorProperties;

	public CustomErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
		super(errorAttributes);
		this.errorAttributes = errorAttributes;
		Assert.notNull(serverProperties, "ServerProperties must not be null");
		ErrorProperties serverPropertiesError = serverProperties.getError();
		Assert.notNull(serverPropertiesError, "ErrorProperties must not be null");
		this.errorProperties = serverPropertiesError;
	}

	@RequestMapping
	@ResponseStatus(HttpStatus.OK)
	public Response<?> error(HttpServletRequest request, HttpServletResponse response) {
		Response<?> error = Response.error();

		HttpStatus status = getStatus(request);
		error.setHttpStatus(status.value());

		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));

		log.error("自定义 错误处理：\n{}", body);

		if (HttpStatus.NOT_FOUND.equals(status)) {
			error.setMessage("访问地址不存在");
			error.setUrl(String.valueOf(body.get("path")));
		}
		else if (HttpStatus.METHOD_NOT_ALLOWED.equals(status)) {
			error.setMessage("不支持当前 HTTP 请求方法");
		}
		else {
			error.setMessage("此异常暂未分类，请联系管理员");
		}

		return error;
	}

	protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
		ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
		if (this.errorProperties.isIncludeException()) {
			options = options.including(ErrorAttributeOptions.Include.EXCEPTION);
		}
		if (isIncludeStackTrace(request, mediaType)) {
			options = options.including(ErrorAttributeOptions.Include.STACK_TRACE);
		}
		if (isIncludeMessage(request, mediaType)) {
			options = options.including(ErrorAttributeOptions.Include.MESSAGE);
		}
		if (isIncludeBindingErrors(request, mediaType)) {
			options = options.including(ErrorAttributeOptions.Include.BINDING_ERRORS);
		}
		return options;
	}

	/**
	 * Determine if the stacktrace attribute should be included.
	 * @param request the source request
	 * @param produces the media type produced (or {@code MediaType.ALL})
	 * @return if the stacktrace attribute should be included
	 */
	protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
		switch (getErrorProperties().getIncludeStacktrace()) {
			case ALWAYS:
				return true;
			case ON_PARAM:
				return getTraceParameter(request);
			default:
				return false;
		}
	}

	/**
	 * Determine if the message attribute should be included.
	 * @param request the source request
	 * @param produces the media type produced (or {@code MediaType.ALL})
	 * @return if the message attribute should be included
	 */
	protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
		switch (getErrorProperties().getIncludeMessage()) {
			case ALWAYS:
				return true;
			case ON_PARAM:
				return getMessageParameter(request);
			default:
				return false;
		}
	}

	/**
	 * Determine if the errors attribute should be included.
	 * @param request the source request
	 * @param produces the media type produced (or {@code MediaType.ALL})
	 * @return if the errors attribute should be included
	 */
	protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType produces) {
		switch (getErrorProperties().getIncludeBindingErrors()) {
			case ALWAYS:
				return true;
			case ON_PARAM:
				return getErrorsParameter(request);
			default:
				return false;
		}
	}

	/**
	 * Provide access to the error properties.
	 * @return the error properties
	 */
	protected ErrorProperties getErrorProperties() {
		return this.errorProperties;
	}

}
